/*
 * Copyright (C) 2006 The Android Open Source Project
 * Copyright (C) 2013 YIXIA.COM
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.vov.vitamio;

import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.text.TextUtils;
import android.util.SparseArray;
import android.view.Surface;
import android.view.SurfaceHolder;

import io.vov.vitamio.utils.FileUtils;
import io.vov.vitamio.utils.Log;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * MediaPlayer class can be used to control playback of audio/video files and
 * streams. An example on how to use the methods in this class can be found in
 * {@link io.vov.vitamio.widget.VideoView}. This class will function the same as
 * android.media.MediaPlayer in most cases. Please see <a
 * href="http://developer.android.com/guide/topics/media/index.html">Audio and
 * Video</a> for additional help using MediaPlayer.
 */
public class MediaPlayer {
	public static final int CACHE_TYPE_NOT_AVAILABLE = 1;
	public static final int CACHE_TYPE_START = 2;
	public static final int CACHE_TYPE_UPDATE = 3;
	public static final int CACHE_TYPE_SPEED = 4;
	public static final int CACHE_TYPE_COMPLETE = 5;
	public static final int CACHE_INFO_NO_SPACE = 1;
	public static final int CACHE_INFO_STREAM_NOT_SUPPORT = 2;
	public static final int MEDIA_ERROR_UNKNOWN = 1;
	public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200;

	/** File or network related operation errors. */
	public static final int MEDIA_ERROR_IO = -5;
	/** Bitstream is not conforming to the related coding standard or file spec. */
	public static final int MEDIA_ERROR_MALFORMED = -1007;
	/**
	 * Bitstream is conforming to the related coding standard or file spec, but
	 * the media framework does not support the feature.
	 */
	public static final int MEDIA_ERROR_UNSUPPORTED = -1010;
	/**
	 * Some operation takes too long to complete, usually more than 3-5 seconds.
	 */
	public static final int MEDIA_ERROR_TIMED_OUT = -110;
	/**
	 * The video is too complex for the decoder: it can't decode frames fast
	 * enough. Possibly only the audio plays fine at this stage.
	 * 
	 * @see io.vov.vitamio.MediaPlayer.OnInfoListener
	 */
	public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700;
	/**
	 * MediaPlayer is temporarily pausing playback internally in order to buffer
	 * more data.
	 */
	public static final int MEDIA_INFO_BUFFERING_START = 701;
	/**
	 * MediaPlayer is resuming playback after filling buffers.
	 */
	public static final int MEDIA_INFO_BUFFERING_END = 702;
	/**
	 * The media cannot be seeked (e.g live stream)
	 * 
	 * @see io.vov.vitamio.MediaPlayer.OnInfoListener
	 */
	public static final int MEDIA_INFO_NOT_SEEKABLE = 801;
	/**
	 * The rate in KB/s of av_read_frame()
	 * 
	 * @see io.vov.vitamio.MediaPlayer.OnInfoListener
	 */
	public static final int MEDIA_INFO_DOWNLOAD_RATE_CHANGED = 901;
	public static final int VIDEOQUALITY_LOW = -16;
	public static final int VIDEOQUALITY_MEDIUM = 0;
	public static final int VIDEOQUALITY_HIGH = 16;

	public static final int VIDEOCHROMA_RGB565 = 0;
	public static final int VIDEOCHROMA_RGBA = 1;
	/**
	 * The subtitle displayed is embeded in the movie
	 */
	public static final int SUBTITLE_INTERNAL = 0;
	/**
	 * The subtitle displayed is an external file
	 */
	public static final int SUBTITLE_EXTERNAL = 1;
	/**
	 * The external subtitle types which Vitamio supports.
	 */
	public static final String[] SUB_TYPES = { ".srt", ".ssa", ".smi", ".txt",
			".sub", ".ass", ".webvtt" };
	private static final int MEDIA_NOP = 0;
	private static final int MEDIA_PREPARED = 1;
	private static final int MEDIA_PLAYBACK_COMPLETE = 2;
	private static final int MEDIA_BUFFERING_UPDATE = 3;
	private static final int MEDIA_SEEK_COMPLETE = 4;
	private static final int MEDIA_SET_VIDEO_SIZE = 5;
	private static final int MEDIA_ERROR = 100;
	private static final int MEDIA_INFO = 200;
	private static final int MEDIA_CACHE = 300;
	private static final int MEDIA_HW_ERROR = 400;
	private static final int MEDIA_TIMED_TEXT = 1000;
	private static final int MEDIA_CACHING_UPDATE = 2000;
	private static final String MEDIA_CACHING_SEGMENTS = "caching_segment";
	private static final String MEDIA_CACHING_TYPE = "caching_type";
	private static final String MEDIA_CACHING_INFO = "caching_info";
	private static final String MEDIA_SUBTITLE_STRING = "sub_string";
	private static final String MEDIA_SUBTITLE_BYTES = "sub_bytes";
	private static final String MEDIA_SUBTITLE_TYPE = "sub_type";
	private static final int SUBTITLE_TEXT = 0;
	private static final int SUBTITLE_BITMAP = 1;
	private static AtomicBoolean NATIVE_OMX_LOADED = new AtomicBoolean(false);
	private Context mContext;
	private Surface mSurface;
	private SurfaceHolder mSurfaceHolder;
	private EventHandler mEventHandler;
	private PowerManager.WakeLock mWakeLock = null;
	private boolean mScreenOnWhilePlaying;
	private boolean mStayAwake;
	private Metadata mMeta;
	private TrackInfo[] mInbandTracks;
	private TrackInfo mOutOfBandTracks;
	private AssetFileDescriptor mFD = null;
	private OnHWRenderFailedListener mOnHWRenderFailedListener;
	private OnPreparedListener mOnPreparedListener;
	private OnCompletionListener mOnCompletionListener;
	private OnBufferingUpdateListener mOnBufferingUpdateListener;
	private OnCachingUpdateListener mOnCachingUpdateListener;
	private OnSeekCompleteListener mOnSeekCompleteListener;
	private OnVideoSizeChangedListener mOnVideoSizeChangedListener;
	private OnErrorListener mOnErrorListener;
	/**
	 * Register a callback to be invoked when an info/warning is available.
	 * 
	 * @param listener
	 *            the callback that will be run
	 */
	private OnInfoListener mOnInfoListener;
	private OnTimedTextListener mOnTimedTextListener;
	private AudioTrack mAudioTrack;
	private int mAudioTrackBufferSize;
	private Surface mLocalSurface;
	private Bitmap mBitmap;
	private ByteBuffer mByteBuffer;

	/**
	 * Default constructor. The same as Android's MediaPlayer().
	 * <p>
	 * When done with the MediaPlayer, you should call {@link #release()}, to
	 * free the resources. If not released, too many MediaPlayer instances may
	 * result in an exception.
	 * </p>
	 */
	public MediaPlayer(Context ctx) {
		this(ctx, false);
	}

	/**
	 * Default constructor. The same as Android's MediaPlayer().
	 * <p>
	 * When done with the MediaPlayer, you should call {@link #release()}, to
	 * free the resources. If not released, too many MediaPlayer instances may
	 * result in an exception.
	 * </p>
	 * 
	 * @param preferHWDecoder
	 *            MediaPlayer will try to use hardware accelerated decoder if
	 *            true
	 */
	public MediaPlayer(Context ctx, boolean preferHWDecoder) {
		mContext = ctx;

		String LIB_ROOT = Vitamio.getLibraryPath();
		if (preferHWDecoder) {
			if (!NATIVE_OMX_LOADED.get()) {
				if (Build.VERSION.SDK_INT > 17)
					loadOMX_native(LIB_ROOT + "libOMX.18.so");
				else if (Build.VERSION.SDK_INT > 13)
					loadOMX_native(LIB_ROOT + "libOMX.14.so");
				else if (Build.VERSION.SDK_INT > 10)
					loadOMX_native(LIB_ROOT + "libOMX.11.so");
				else
					loadOMX_native(LIB_ROOT + "libOMX.9.so");
				NATIVE_OMX_LOADED.set(true);
			}
		} else {
			try {
				unloadOMX_native();
			} catch (UnsatisfiedLinkError e) {
				Log.e("unloadOMX failed %s", e.toString());
			}
			NATIVE_OMX_LOADED.set(false);
		}

		Looper looper;
		if ((looper = Looper.myLooper()) != null)
			mEventHandler = new EventHandler(this, looper);
		else if ((looper = Looper.getMainLooper()) != null)
			mEventHandler = new EventHandler(this, looper);
		else
			mEventHandler = null;

		native_init();
	}

	static {
		String LIB_ROOT = Vitamio.getLibraryPath();
		try {
			Log.i("LIB ROOT: %s", LIB_ROOT);
			System.load(LIB_ROOT + "libstlport_shared.so");
			System.load(LIB_ROOT + "libvplayer.so");
			loadFFmpeg_native(LIB_ROOT + "libffmpeg.so");
			boolean vvo_loaded = false;
			if (Build.VERSION.SDK_INT > 8)
				vvo_loaded = loadVVO_native(LIB_ROOT + "libvvo.9.so");
			else if (Build.VERSION.SDK_INT > 7)
				vvo_loaded = loadVVO_native(LIB_ROOT + "libvvo.8.so");
			else
				vvo_loaded = loadVVO_native(LIB_ROOT + "libvvo.7.so");
			if (!vvo_loaded) {
				vvo_loaded = loadVVO_native(LIB_ROOT + "libvvo.j.so");
				Log.d("FALLBACK TO VVO JNI " + vvo_loaded);
			}
			loadVAO_native(LIB_ROOT + "libvao.0.so");
		} catch (java.lang.UnsatisfiedLinkError e) {
			Log.e("Error loading libs", e);
		}
	}

	private static void postEventFromNative(Object mediaplayer_ref, int what,
			int arg1, int arg2, Object obj) {
		MediaPlayer mp = (MediaPlayer) (mediaplayer_ref);
		if (mp == null)
			return;

		if (mp.mEventHandler != null) {
			Message m = mp.mEventHandler.obtainMessage(what, arg1, arg2, obj);
			mp.mEventHandler.sendMessage(m);
		}
	}

	private static native boolean loadVAO_native(String vaoPath);

	private static native boolean loadVVO_native(String vvoPath);

	private static native boolean loadOMX_native(String omxPath);

	private static native void unloadOMX_native();

	private static native boolean loadFFmpeg_native(String ffmpegPath);

	private native void _setVideoSurface(Surface surface);

	/**
	 * Sets the SurfaceHolder to use for displaying the video portion of the
	 * media. This call is optional. Not calling it when playing back a video
	 * will result in only the audio track being played.
	 * 
	 * @param sh
	 *            the SurfaceHolder to use for video display
	 */
	public void setDisplay(SurfaceHolder sh) {
		if (sh == null) {
			releaseDisplay();
		} else {
			mSurfaceHolder = sh;
			mSurface = sh.getSurface();
			_setVideoSurface(mSurface);
			updateSurfaceScreenOn();
		}
	}

	/**
	 * Sets the Surface to use for displaying the video portion of the media.
	 * This is similar to {@link #setDisplay(SurfaceHolder)}.
	 * 
	 * @param surface
	 *            the Surface to use for video display
	 */
	public void setSurface(Surface surface) {
		if (surface == null) {
			releaseDisplay();
		} else {
			mSurfaceHolder = null;
			mSurface = surface;
			_setVideoSurface(mSurface);
			updateSurfaceScreenOn();
		}
	}

	/**
	 * Sets the data source (file-path or http/rtsp URL) to use.
	 * 
	 * @param path
	 *            the path of the file, or the http/rtsp URL of the stream you
	 *            want to play
	 * @throws IllegalStateException
	 *             if it is called in an invalid state
	 *             <p/>
	 *             <p/>
	 *             When <code>path</code> refers to a local file, the file may
	 *             actually be opened by a process other than the calling
	 *             application. This implies that the pathname should be an
	 *             absolute path (as any other process runs with unspecified
	 *             current working directory), and that the pathname should
	 *             reference a world-readable file. As an alternative, the
	 *             application could first open the file for reading, and then
	 *             use the file descriptor form
	 *             {@link #setDataSource(FileDescriptor)}.
	 */
	public void setDataSource(String path) throws IOException,
			IllegalArgumentException, SecurityException, IllegalStateException {
		_setDataSource(path, null, null);
	}

	/**
	 * Sets the data source as a content Uri.
	 * 
	 * @param context
	 *            the Context to use when resolving the Uri
	 * @param uri
	 *            the Content URI of the data you want to play
	 * @throws IllegalStateException
	 *             if it is called in an invalid state
	 */
	public void setDataSource(Context context, Uri uri) throws IOException,
			IllegalArgumentException, SecurityException, IllegalStateException {
		setDataSource(context, uri, null);
	}

	public void setDataSource(Context context, Uri uri,
			Map<String, String> headers) throws IOException,
			IllegalArgumentException, SecurityException, IllegalStateException {
		if (context == null || uri == null)
			throw new IllegalArgumentException();
		String scheme = uri.getScheme();
		if (scheme == null || scheme.equals("file")) {
			setDataSource(FileUtils.getPath(uri.toString()));
			return;
		}

		try {
			ContentResolver resolver = context.getContentResolver();
			mFD = resolver.openAssetFileDescriptor(uri, "r");
			if (mFD == null)
				return;
			setDataSource(mFD.getParcelFileDescriptor().getFileDescriptor());
			return;
		} catch (Exception e) {
			closeFD();
		}
		setDataSource(uri.toString(), headers);
	}

	/**
	 * Sets the data source (file-path or http/rtsp URL) to use.
	 * 
	 * @param path
	 *            the path of the file, or the http/rtsp URL of the stream you
	 *            want to play
	 * @param headers
	 *            the headers associated with the http request for the stream
	 *            you want to play
	 * @throws IllegalStateException
	 *             if it is called in an invalid state
	 */
	public void setDataSource(String path, Map<String, String> headers)
			throws IOException, IllegalArgumentException, SecurityException,
			IllegalStateException {
		String[] keys = null;
		String[] values = null;

		if (headers != null) {
			keys = new String[headers.size()];
			values = new String[headers.size()];

			int i = 0;
			for (Map.Entry<String, String> entry : headers.entrySet()) {
				keys[i] = entry.getKey();
				values[i] = entry.getValue();
				++i;
			}
		}
		setDataSource(path, keys, values);
	}

	/**
	 * Sets the data source (file-path or http/rtsp URL) to use.
	 * 
	 * @param path
	 *            the path of the file, or the http/rtsp URL of the stream you
	 *            want to play
	 * @param keys
	 *            AVOption key
	 * @param values
	 *            AVOption value
	 * @throws IllegalStateException
	 *             if it is called in an invalid state
	 */
	public void setDataSource(String path, String[] keys, String[] values)
			throws IOException, IllegalArgumentException, SecurityException,
			IllegalStateException {
		final Uri uri = Uri.parse(path);
		if ("file".equals(uri.getScheme())) {
			path = uri.getPath();
		}

		final File file = new File(path);
		if (file.exists()) {
			FileInputStream is = new FileInputStream(file);
			FileDescriptor fd = is.getFD();
			setDataSource(fd);
			is.close();
		} else {
			_setDataSource(path, keys, values);
		}
	}

	/**
	 * Set the segments source url
	 * 
	 * @param segments
	 *            the array path of the url e.g. Segmented video list
	 * @param cacheDir
	 *            e.g. getCacheDir().toString()
	 */
	public void setDataSegments(String[] uris, String cacheDir) {
		_setDataSegmentsSource(uris, cacheDir);
	}

	public void setOnHWRenderFailedListener(OnHWRenderFailedListener l) {
		mOnHWRenderFailedListener = l;
	}

	/**
	 * Sets the data source (file-path or http/rtsp/mms URL) to use.
	 * 
	 * @param path
	 *            the path of the file, or the http/rtsp/mms URL of the stream
	 *            you want to play
	 * @param keys
	 *            AVOption key
	 * @param values
	 *            AVOption value
	 * @throws IllegalStateException
	 *             if it is called in an invalid state
	 */
	private native void _setDataSource(String path, String[] keys,
			String[] values) throws IOException, IllegalArgumentException,
			IllegalStateException;

	/**
	 * Sets the data source (FileDescriptor) to use. It is the caller's
	 * responsibility to close the file descriptor. It is safe to do so as soon
	 * as this call returns.
	 * 
	 * @param fd
	 *            the FileDescriptor for the file you want to play
	 * @throws IllegalStateException
	 *             if it is called in an invalid state
	 */
	public native void setDataSource(FileDescriptor fd) throws IOException,
			IllegalArgumentException, IllegalStateException;

	/**
	 * Set the segments source url
	 * 
	 * @param segments
	 *            the array path of the url
	 * @param cacheDir
	 *            e.g. getCacheDir().toString()
	 */
	private native void _setDataSegmentsSource(String[] segments,
			String cacheDir);

	/**
	 * Prepares the player for playback, synchronously.
	 * <p/>
	 * After setting the datasource and the display surface, you need to either
	 * call prepare() or prepareAsync(). For files, it is OK to call prepare(),
	 * which blocks until MediaPlayer is ready for playback.
	 * 
	 * @throws IllegalStateException
	 *             if it is called in an invalid state
	 */
	public native void prepare() throws IOException, IllegalStateException;

	/**
	 * Prepares the player for playback, asynchronously.
	 * <p/>
	 * After setting the datasource and the display surface, you need to either
	 * call prepare() or prepareAsync(). For streams, you should call
	 * prepareAsync(), which returns immediately, rather than blocking until
	 * enough data has been buffered.
	 * 
	 * @throws IllegalStateException
	 *             if it is called in an invalid state
	 */
	public native void prepareAsync() throws IllegalStateException;

	/**
	 * Starts or resumes playback. If playback had previously been paused,
	 * playback will continue from where it was paused. If playback had been
	 * stopped, or never started before, playback will start at the beginning.
	 * 
	 * @throws IllegalStateException
	 *             if it is called in an invalid state
	 */
	public void start() throws IllegalStateException {
		stayAwake(true);
		_start();
	}

	private native void _start() throws IllegalStateException;

	/**
	 * The same as {@link #pause()}
	 * 
	 * @throws IllegalStateException
	 *             if the internal player engine has not been initialized.
	 */
	public void stop() throws IllegalStateException {
		stayAwake(false);
		_stop();
	}

	private native void _stop() throws IllegalStateException;

	/**
	 * Pauses playback. Call start() to resume.
	 * 
	 * @throws IllegalStateException
	 *             if the internal player engine has not been initialized.
	 */
	public void pause() throws IllegalStateException {
		stayAwake(false);
		_pause();
	}

	private native void _pause() throws IllegalStateException;

	/**
	 * Set the low-level power management behavior for this MediaPlayer. This
	 * can be used when the MediaPlayer is not playing through a SurfaceHolder
	 * set with {@link #setDisplay(SurfaceHolder)} and thus can use the
	 * high-level {@link #setScreenOnWhilePlaying(boolean)} feature.
	 * <p/>
	 * This function has the MediaPlayer access the low-level power manager
	 * service to control the device's power usage while playing is occurring.
	 * The parameter is a combination of {@link android.os.PowerManager} wake
	 * flags. Use of this method requires
	 * {@link android.Manifest.permission#WAKE_LOCK} permission. By default, no
	 * attempt is made to keep the device awake during playback.
	 * 
	 * @param context
	 *            the Context to use
	 * @param mode
	 *            the power/wake mode to set
	 * @see android.os.PowerManager
	 */
	@SuppressLint("Wakelock")
	public void setWakeMode(Context context, int mode) {
		boolean washeld = false;
		if (mWakeLock != null) {
			if (mWakeLock.isHeld()) {
				washeld = true;
				mWakeLock.release();
			}
			mWakeLock = null;
		}

		PowerManager pm = (PowerManager) context
				.getSystemService(Context.POWER_SERVICE);
		mWakeLock = pm.newWakeLock(mode | PowerManager.ON_AFTER_RELEASE,
				MediaPlayer.class.getName());
		mWakeLock.setReferenceCounted(false);
		if (washeld) {
			mWakeLock.acquire();
		}
	}

	/**
	 * Control whether we should use the attached SurfaceHolder to keep the
	 * screen on while video playback is occurring. This is the preferred method
	 * over {@link #setWakeMode} where possible, since it doesn't require that
	 * the application have permission for low-level wake lock access.
	 * 
	 * @param screenOn
	 *            Supply true to keep the screen on, false to allow it to turn
	 *            off.
	 */
	public void setScreenOnWhilePlaying(boolean screenOn) {
		if (mScreenOnWhilePlaying != screenOn) {
			mScreenOnWhilePlaying = screenOn;
			updateSurfaceScreenOn();
		}
	}

	@SuppressLint("Wakelock")
	private void stayAwake(boolean awake) {
		if (mWakeLock != null) {
			if (awake && !mWakeLock.isHeld()) {
				mWakeLock.acquire();
			} else if (!awake && mWakeLock.isHeld()) {
				mWakeLock.release();
			}
		}
		mStayAwake = awake;
		updateSurfaceScreenOn();
	}

	private void updateSurfaceScreenOn() {
		if (mSurfaceHolder != null)
			mSurfaceHolder.setKeepScreenOn(mScreenOnWhilePlaying && mStayAwake);
	}

	/**
	 * Returns the width of the video.
	 * 
	 * @return the width of the video, or 0 if there is no video, or the width
	 *         has not been determined yet. The OnVideoSizeChangedListener can
	 *         be registered via
	 *         {@link #setOnVideoSizeChangedListener(OnVideoSizeChangedListener)}
	 *         to provide a notification when the width is available.
	 */
	public native int getVideoWidth();

	private native int getVideoWidth_a();

	/**
	 * Returns the height of the video.
	 * 
	 * @return the height of the video, or 0 if there is no video, or the height
	 *         has not been determined yet. The OnVideoSizeChangedListener can
	 *         be registered via
	 *         {@link #setOnVideoSizeChangedListener(OnVideoSizeChangedListener)}
	 *         to provide a notification when the height is available.
	 */
	public native int getVideoHeight();

	private native int getVideoHeight_a();

	/**
	 * Checks whether the MediaPlayer is playing.
	 * 
	 * @return true if currently playing, false otherwise
	 */
	public native boolean isPlaying();

	/**
	 * Set whether cache the online playback file
	 * 
	 * @param cache
	 */
	public native void setUseCache(boolean cache);

	/**
	 * set cache file dir
	 * 
	 * @param directory
	 */
	public native void setCacheDirectory(String directory);

	/**
	 * Adaptive streaming support, default is false
	 * 
	 * @param adaptive
	 *            true if wanna adaptive steam
	 * 
	 */
	public native void setAdaptiveStream(boolean adaptive);

	/**
	 * Seeks to specified time position.
	 * 
	 * @param msec
	 *            the offset in milliseconds from the start to seek to
	 * @throws IllegalStateException
	 *             if the internal player engine has not been initialized
	 */
	public native void seekTo(long msec) throws IllegalStateException;

	/**
	 * Gets the current playback position.
	 * 
	 * @return the current position in milliseconds
	 */
	public native long getCurrentPosition();

	/**
	 * Get the current video frame
	 * 
	 * @return bitmap object
	 */
	public native Bitmap getCurrentFrame();

	/**
	 * Gets the duration of the file.
	 * 
	 * @return the duration in milliseconds
	 */
	public native long getDuration();

	/**
	 * Gets the media metadata.
	 * 
	 * @return The metadata, possibly empty. null if an error occurred.
	 */
	public Metadata getMetadata() {
		if (mMeta == null) {
			mMeta = new Metadata();
			Map<byte[], byte[]> meta = new HashMap<byte[], byte[]>();

			if (!native_getMetadata(meta)) {
				return null;
			}

			if (!mMeta.parse(meta, getMetaEncoding())) {
				return null;
			}
		}
		return mMeta;
	}

	/**
	 * Releases resources associated with this MediaPlayer object. It is
	 * considered good practice to call this method when you're done using the
	 * MediaPlayer.
	 */
	public void release() {
		stayAwake(false);
		updateSurfaceScreenOn();
		mOnPreparedListener = null;
		mOnBufferingUpdateListener = null;
		mOnCompletionListener = null;
		mOnSeekCompleteListener = null;
		mOnErrorListener = null;
		mOnInfoListener = null;
		mOnVideoSizeChangedListener = null;
		mOnCachingUpdateListener = null;
		mOnHWRenderFailedListener = null;
		_release();
		closeFD();
	}

	private native void _release();

	/**
	 * Resets the MediaPlayer to its uninitialized state. After calling this
	 * method, you will have to initialize it again by setting the data source
	 * and calling prepare().
	 */
	public void reset() {
		stayAwake(false);
		_reset();
		mEventHandler.removeCallbacksAndMessages(null);
		closeFD();
	}

	private native void _reset();

	private void closeFD() {
		if (mFD != null) {
			try {
				mFD.close();
			} catch (IOException e) {
				Log.e("closeFD", e);
			}
			mFD = null;
		}
	}

	/**
	 * Sets the player to be looping or non-looping.
	 * 
	 * @param looping
	 *            whether to loop or not
	 */
	public native void setLooping(boolean looping);

	/**
	 * Checks whether the MediaPlayer is looping or non-looping.
	 * 
	 * @return true if the MediaPlayer is currently looping, false otherwise
	 */
	public native boolean isLooping();

	/**
	 * Amplify audio
	 * 
	 * @param ratio
	 *            e.g. 3.5
	 */
	public native void setAudioAmplify(float ratio);

	public native void setVolume(float leftVolume, float rightVolume);

	private native final boolean native_getTrackInfo(
			SparseArray<byte[]> trackSparse);

	private native final boolean native_getMetadata(Map<byte[], byte[]> meta);

	private native final void native_init();

	private native final void native_finalize();

	/**
	 * Returns an array of track information.
	 * 
	 * @return Array of track info. The total number of tracks is the array
	 *         length. Must be called again if an external timed text source has
	 *         been added after any of the addTimedTextSource methods are
	 *         called.
	 */
	public TrackInfo[] getTrackInfo(String encoding) {
		TrackInfo[] trackInfo = getInbandTrackInfo(encoding);
		// add out-of-band tracks
		String timedTextPath = getTimedTextPath();
		if (TextUtils.isEmpty(timedTextPath)) {
			return trackInfo;
		}
		TrackInfo[] allTrackInfo = new TrackInfo[trackInfo.length + 1];
		System.arraycopy(trackInfo, 0, allTrackInfo, 0, trackInfo.length);
		int i = trackInfo.length;
		SparseArray<MediaFormat> trackInfoArray = new SparseArray<MediaFormat>();
		MediaFormat mediaFormat = new MediaFormat();
		mediaFormat.setString(MediaFormat.KEY_TITLE,
				timedTextPath.substring(timedTextPath.lastIndexOf("/")));
		mediaFormat.setString(MediaFormat.KEY_PATH, timedTextPath);
		SparseArray<MediaFormat> timedTextSparse = findTrackFromTrackInfo(
				TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT, trackInfo);
		if (timedTextSparse == null || timedTextSparse.size() == 0)
			trackInfoArray.put(timedTextSparse.keyAt(0), mediaFormat);
		else
			trackInfoArray.put(
					timedTextSparse.keyAt(timedTextSparse.size() - 1),
					mediaFormat);
		mOutOfBandTracks = new TrackInfo(TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE,
				trackInfoArray);
		allTrackInfo[i] = mOutOfBandTracks;
		return allTrackInfo;
	}

	private TrackInfo[] getInbandTrackInfo(String encoding) {
		if (mInbandTracks == null) {
			SparseArray<byte[]> trackSparse = new SparseArray<byte[]>();
			if (!native_getTrackInfo(trackSparse)) {
				return null;
			}

			int size = trackSparse.size();
			mInbandTracks = new TrackInfo[size];
			for (int i = 0; i < size; i++) {
				SparseArray<MediaFormat> sparseArray = parseTrackInfo(
						trackSparse.valueAt(i), encoding);
				TrackInfo trackInfo = new TrackInfo(trackSparse.keyAt(i),
						sparseArray);
				mInbandTracks[i] = trackInfo;
			}
		}
		return mInbandTracks;
	}

	/**
	 * Use default chartset {@link #getTrackInfo()} method.
	 * 
	 * @return array of {@link TrackInfo}
	 */
	public TrackInfo[] getTrackInfo() {
		return getTrackInfo(Charset.defaultCharset().name());
	}

	private SparseArray<MediaFormat> parseTrackInfo(byte[] tracks,
			String encoding) {
		SparseArray<MediaFormat> trackSparse = new SparseArray<MediaFormat>();
		String trackString;
		int trackNum;
		try {
			trackString = new String(tracks, encoding);
		} catch (Exception e) {
			Log.e("getTrackMap exception");
			trackString = new String(tracks);
		}
		for (String s : trackString.split("!#!")) {
			try {
				MediaFormat mediaFormat = null;
				String[] formats = s.split("\\.");
				if (formats == null)
					continue;
				trackNum = Integer.parseInt(formats[0]);
				if (formats.length == 3) {
					mediaFormat = MediaFormat.createSubtitleFormat(formats[2],
							formats[1]);
				} else if (formats.length == 2) {
					mediaFormat = MediaFormat.createSubtitleFormat("",
							formats[1]);
				}
				trackSparse.put(trackNum, mediaFormat);
			} catch (NumberFormatException e) {
			}
		}

		return trackSparse;
	}

	/**
	 * @param mediaTrackType
	 * @param trackInfo
	 * @return {@link TrackInfo#getTrackInfoArray()}
	 */
	public SparseArray<MediaFormat> findTrackFromTrackInfo(int mediaTrackType,
			TrackInfo[] trackInfo) {
		for (int i = 0; i < trackInfo.length; i++) {
			if (trackInfo[i].getTrackType() == mediaTrackType) {
				return trackInfo[i].getTrackInfoArray();
			}
		}
		return null;
	}

	/**
	 * Set the file-path of an external timed text.
	 * 
	 * @param path
	 *            must be a local file
	 */
	public native void addTimedTextSource(String path);

	/**
	 * Selects a track.
	 * <p>
	 * In any valid state, if it is called multiple times on the same type of
	 * track (ie. Video, Audio, Timed Text), the most recent one will be chosen.
	 * </p>
	 * <p>
	 * The first audio and video tracks are selected by default if available,
	 * even though this method is not called. However, no timed text track will
	 * be selected until this function is called.
	 * </p>
	 * 
	 * @param index
	 *            the index of the track to be selected. The valid range of the
	 *            index is 0..total number of track - 1. The total number of
	 *            tracks as well as the type of each individual track can be
	 *            found by calling {@link #getTrackInfo()} method.
	 * @see io.vov.vitamio.MediaPlayer#getTrackInfo
	 */
	public void selectTrack(int index) {
		selectOrDeselectBandTrack(index, true /* select */);
	}

	/**
	 * Deselect a track.
	 * <p>
	 * Currently, the track must be a timed text track and no audio or video
	 * tracks can be deselected.
	 * </p>
	 * 
	 * @param index
	 *            the index of the track to be deselected. The valid range of
	 *            the index is 0..total number of tracks - 1. The total number
	 *            of tracks as well as the type of each individual track can be
	 *            found by calling {@link #getTrackInfo()} method.
	 * @see io.vov.vitamio.MediaPlayer#getTrackInfo
	 */
	public void deselectTrack(int index) {
		selectOrDeselectBandTrack(index, false /* select */);
	}

	private void selectOrDeselectBandTrack(int index, boolean select) {
		if (mOutOfBandTracks != null) {
			SparseArray<MediaFormat> mediaSparse = mOutOfBandTracks
					.getTrackInfoArray();
			int trackIndex = mediaSparse.keyAt(0);
			MediaFormat mediaFormat = mediaSparse.valueAt(0);
			if (index == trackIndex && select) {
				addTimedTextSource(mediaFormat.getString(MediaFormat.KEY_PATH));
				return;
			}
		}
		selectOrDeselectTrack(index, select);
	}

	private native void selectOrDeselectTrack(int index, boolean select);

	@Override
	protected void finalize() {
		native_finalize();
	}

	/**
	 * Register a callback to be invoked when the media source is ready for
	 * playback.
	 * 
	 * @param listener
	 *            the callback that will be run
	 */
	public void setOnPreparedListener(OnPreparedListener listener) {
		mOnPreparedListener = listener;
	}

	/**
	 * Register a callback to be invoked when the end of a media source has been
	 * reached during playback.
	 * 
	 * @param listener
	 *            the callback that will be run
	 */
	public void setOnCompletionListener(OnCompletionListener listener) {
		mOnCompletionListener = listener;
	}

	/**
	 * Register a callback to be invoked when the status of a network stream's
	 * buffer has changed.
	 * 
	 * @param listener
	 *            the callback that will be run.
	 */
	public void setOnBufferingUpdateListener(OnBufferingUpdateListener listener) {
		mOnBufferingUpdateListener = listener;
	}

	/**
	 * Register a callback to be invoked when the segments cached on storage has
	 * changed.
	 * 
	 * @param listener
	 *            the callback that will be run.
	 */
	public void setOnCachingUpdateListener(OnCachingUpdateListener listener) {
		mOnCachingUpdateListener = listener;
	}

	private void updateCacheStatus(int type, int info, long[] segments) {
		if (mEventHandler != null) {
			Message m = mEventHandler.obtainMessage(MEDIA_CACHING_UPDATE);
			Bundle b = m.getData();
			b.putInt(MEDIA_CACHING_TYPE, type);
			b.putInt(MEDIA_CACHING_INFO, info);
			b.putLongArray(MEDIA_CACHING_SEGMENTS, segments);
			mEventHandler.sendMessage(m);
		}
	}

	/**
	 * Register a callback to be invoked when a seek operation has been
	 * completed.
	 * 
	 * @param listener
	 *            the callback that will be run
	 */
	public void setOnSeekCompleteListener(OnSeekCompleteListener listener) {
		mOnSeekCompleteListener = listener;
	}

	/**
	 * Register a callback to be invoked when the video size is known or
	 * updated.
	 * 
	 * @param listener
	 *            the callback that will be run
	 */
	public void setOnVideoSizeChangedListener(
			OnVideoSizeChangedListener listener) {
		mOnVideoSizeChangedListener = listener;
	}

	/**
	 * Register a callback to be invoked when an error has happened during an
	 * asynchronous operation.
	 * 
	 * @param listener
	 *            the callback that will be run
	 */
	public void setOnErrorListener(OnErrorListener listener) {
		mOnErrorListener = listener;
	}

	public void setOnInfoListener(OnInfoListener listener) {
		mOnInfoListener = listener;
	}

	/**
	 * Register a callback to be invoked when a timed text need to display.
	 * 
	 * @param listener
	 *            the callback that will be run
	 */
	public void setOnTimedTextListener(OnTimedTextListener listener) {
		mOnTimedTextListener = listener;
	}

	private void updateSub(int subType, byte[] bytes, String encoding,
			int width, int height) {
		if (mEventHandler != null) {
			Message m = mEventHandler.obtainMessage(MEDIA_TIMED_TEXT, width,
					height);
			Bundle b = m.getData();
			if (subType == SUBTITLE_TEXT) {
				b.putInt(MEDIA_SUBTITLE_TYPE, SUBTITLE_TEXT);
				if (encoding == null) {
					b.putString(MEDIA_SUBTITLE_STRING, new String(bytes));
				} else {
					try {
						b.putString(MEDIA_SUBTITLE_STRING, new String(bytes,
								encoding.trim()));
					} catch (UnsupportedEncodingException e) {
						Log.e("updateSub", e);
						b.putString(MEDIA_SUBTITLE_STRING, new String(bytes));
					}
				}
			} else if (subType == SUBTITLE_BITMAP) {
				b.putInt(MEDIA_SUBTITLE_TYPE, SUBTITLE_BITMAP);
				b.putByteArray(MEDIA_SUBTITLE_BYTES, bytes);
			}
			mEventHandler.sendMessage(m);
		}
	}

	protected native void _releaseVideoSurface();

	/**
	 * Calling this result in only the audio track being played.
	 */
	public void releaseDisplay() {
		if (mSurfaceHolder != null) {
			_releaseVideoSurface();
		}
		mSurfaceHolder = null;
		mSurface = null;
	}

	/**
	 * Returns the aspect ratio of the video.
	 * 
	 * @return the aspect ratio of the video, or 0 if there is no video, or the
	 *         width and height is not available.
	 * @see io.vov.vitamio.widget.VideoView#setVideoLayout(int, float)
	 */
	public native float getVideoAspectRatio();

	/**
	 * Set the quality when play video, if the video is too lag, you may try
	 * VIDEOQUALITY_LOW, default is VIDEOQUALITY_LOW.
	 * 
	 * @param quality
	 *            <ul>
	 *            <li>{@link #VIDEOQUALITY_HIGH}
	 *            <li>{@link #VIDEOQUALITY_MEDIUM}
	 *            <li>{@link #VIDEOQUALITY_LOW}
	 *            </ul>
	 */
	public native void setVideoQuality(int quality);

	/**
	 * Set the Video Chroma quality when play video, default is
	 * VIDEOCHROMA_RGB565 You can set on after {@link #prepareAsync()}.
	 * 
	 * @param chroma
	 *            <ul>
	 *            <li>{@link #VIDEOCHROMA_RGB565}
	 *            <li>{@link #VIDEOCHROMA_RGBA}
	 *            </ul>
	 */
	public native void setVideoChroma(int chroma);

	/**
	 * Set if should deinterlace the video picture
	 * 
	 * @param deinterlace
	 */
	public native void setDeinterlace(boolean deinterlace);

	/**
	 * The buffer to fill before playback, default is 1024*1024 Byte
	 * 
	 * @param bufSize
	 *            buffer size in Byte
	 */
	public native void setBufferSize(long bufSize);

	/**
	 * Set video and audio playback speed
	 * 
	 * @param speed
	 *            e.g. 0.8 or 2.0, default to 1.0, range in [0.5-2]
	 */
	public native void setPlaybackSpeed(float speed);

	/**
	 * Checks whether the buffer is filled
	 * 
	 * @return false if buffer is filled
	 */
	public native boolean isBuffering();

	/**
	 * @return the percent
	 * @see io.vov.vitamio.MediaPlayer.OnBufferingUpdateListener
	 */
	public native int getBufferProgress();

	/**
	 * Get the encoding if haven't set with {@link #setMetaEncoding(String)}
	 * 
	 * @return the encoding
	 */
	public native String getMetaEncoding();

	/**
	 * Set the encoding MediaPlayer will use to determine the metadata
	 * 
	 * @param encoding
	 *            e.g. "UTF-8"
	 */
	public native void setMetaEncoding(String encoding);

	/**
	 * Get the audio track number in playback
	 * 
	 * @return track number
	 */
	public native int getAudioTrack();

	/**
	 * Get the video track number in playback
	 * 
	 * @return track number
	 */
	public native int getVideoTrack();

	/**
	 * Tell the MediaPlayer whether to show timed text
	 * 
	 * @param shown
	 *            true if wanna show
	 */
	public native void setTimedTextShown(boolean shown);

	/**
	 * Set the encoding to display timed text.
	 * 
	 * @param encoding
	 *            MediaPlayer will detet it if null
	 */
	public native void setTimedTextEncoding(String encoding);

	/**
	 * @return <ul>
	 *         <li>{@link #SUBTITLE_EXTERNAL}
	 *         <li>{@link #SUBTITLE_INTERNAL}
	 *         </ul>
	 */
	public native int getTimedTextLocation();

	/**
	 * You can get the file-path of the external subtitle in use.
	 * 
	 * @return null if no external subtitle
	 */
	public native String getTimedTextPath();

	/**
	 * Get the subtitle track number in playback
	 * 
	 * @return track number
	 */
	public native int getTimedTextTrack();

	private int audioTrackInit(int sampleRateInHz, int channels) {
		audioTrackRelease();
		int channelConfig = channels >= 2 ? AudioFormat.CHANNEL_OUT_STEREO
				: AudioFormat.CHANNEL_OUT_MONO;
		try {
			mAudioTrackBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz,
					channelConfig, AudioFormat.ENCODING_PCM_16BIT);
			mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
					sampleRateInHz, channelConfig,
					AudioFormat.ENCODING_PCM_16BIT, mAudioTrackBufferSize,
					AudioTrack.MODE_STREAM);
		} catch (Exception e) {
			mAudioTrackBufferSize = 0;
			Log.e("audioTrackInit", e);
		}
		return mAudioTrackBufferSize;
	}

	private void audioTrackSetVolume(float leftVolume, float rightVolume) {
		if (mAudioTrack != null)
			mAudioTrack.setStereoVolume(leftVolume, rightVolume);
	}

	private void audioTrackWrite(byte[] audioData, int offsetInBytes,
			int sizeInBytes) {
		if (mAudioTrack != null
				&& mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
			int written;
			while (sizeInBytes > 0) {
				written = sizeInBytes > mAudioTrackBufferSize ? mAudioTrackBufferSize
						: sizeInBytes;
				mAudioTrack.write(audioData, offsetInBytes, written);
				sizeInBytes -= written;
				offsetInBytes += written;
			}
		}
	}

	private void audioTrackStart() {
		if (mAudioTrack != null
				&& mAudioTrack.getState() == AudioTrack.STATE_INITIALIZED
				&& mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING)
			mAudioTrack.play();
	}

	private void audioTrackPause() {
		if (mAudioTrack != null
				&& mAudioTrack.getState() == AudioTrack.STATE_INITIALIZED)
			mAudioTrack.pause();
	}

	private void audioTrackRelease() {
		if (mAudioTrack != null) {
			if (mAudioTrack.getState() == AudioTrack.STATE_INITIALIZED)
				mAudioTrack.stop();
			mAudioTrack.release();
		}
		mAudioTrack = null;
	}

	public int getAudioSessionId() {
		return mAudioTrack.getAudioSessionId();
	}

	private ByteBuffer surfaceInit() {
		synchronized (this) {
			mLocalSurface = mSurface;
			int w = getVideoWidth_a();
			int h = getVideoHeight_a();
			if (mLocalSurface != null && w != 0 && h != 0) {
				mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.RGB_565);
				mByteBuffer = ByteBuffer.allocateDirect(w * h * 2);
			} else {
				mBitmap = null;
				mByteBuffer = null;
			}
			return mByteBuffer;
		}
	}

	private void surfaceRender() {
		synchronized (this) {
			if (mLocalSurface == null || !mLocalSurface.isValid()
					|| mBitmap == null || mByteBuffer == null)
				return;

			try {
				Canvas c = mLocalSurface.lockCanvas(null);
				mBitmap.copyPixelsFromBuffer(mByteBuffer);
				c.drawBitmap(mBitmap, 0, 0, null);
				mLocalSurface.unlockCanvasAndPost(c);
			} catch (Exception e) {
				Log.e("surfaceRender", e);
			}
		}
	}

	private void surfaceRelease() {
		synchronized (this) {
			mLocalSurface = null;
			mBitmap = null;
			mByteBuffer = null;
		}
	}

	public interface OnHWRenderFailedListener {
		public void onFailed();
	}

	public interface OnPreparedListener {
		/**
		 * Called when the media file is ready for playback.
		 * 
		 * @param mp
		 *            the MediaPlayer that is ready for playback
		 */
		void onPrepared(MediaPlayer mp);
	}

	public interface OnCompletionListener {
		/**
		 * Called when the end of a media source is reached during playback.
		 * 
		 * @param mp
		 *            the MediaPlayer that reached the end of the file
		 */
		void onCompletion(MediaPlayer mp);
	}

	public interface OnBufferingUpdateListener {
		/**
		 * Called to update status in buffering a media stream. Buffering is
		 * storing data in memory while caching on external storage.
		 * 
		 * @param mp
		 *            the MediaPlayer the update pertains to
		 * @param percent
		 *            the percentage (0-100) of the buffer that has been filled
		 *            thus far
		 */
		void onBufferingUpdate(MediaPlayer mp, int percent);
	}

	public interface OnCachingUpdateListener {
		/**
		 * Called to update status in caching a media stream. Caching is storing
		 * data on external storage while buffering in memory.
		 * 
		 * @param mp
		 *            the MediaPlayer the update pertains to
		 * @param segments
		 *            the cached segments in bytes, in format [s1begin, s1end,
		 *            s2begin, s2end], s1begin < s1end < s2begin < s2end. e.g.
		 *            [124, 100423, 4321412, 214323433]
		 */
		void onCachingUpdate(MediaPlayer mp, long[] segments);

		/**
		 * Cache speed
		 * 
		 * @param mp
		 *            the MediaPlayer the update pertains to
		 * @param speed
		 *            the cached speed size kb/s
		 */
		void onCachingSpeed(MediaPlayer mp, int speed);

		/**
		 * Cache start
		 * 
		 * @param mp
		 */
		void onCachingStart(MediaPlayer mp);

		/**
		 * Cache compelete
		 */
		void onCachingComplete(MediaPlayer mp);

		/**
		 * Cache not available
		 * 
		 * @param mp
		 *            the MediaPlayer the update pertains to
		 * @param info
		 *            the not available info
		 *            <ul>
		 *            <li>{@link #CACHE_INFO_NO_SPACE}
		 *            <li>{@link #CACHE_INFO_STREAM_NOT_SUPPORT}
		 *            </ul>
		 */
		void onCachingNotAvailable(MediaPlayer mp, int info);
	}

	public interface OnSeekCompleteListener {
		/**
		 * Called to indicate the completion of a seek operation.
		 * 
		 * @param mp
		 *            the MediaPlayer that issued the seek operation
		 */
		public void onSeekComplete(MediaPlayer mp);
	}

	public interface OnVideoSizeChangedListener {
		/**
		 * Called to indicate the video size
		 * 
		 * @param mp
		 *            the MediaPlayer associated with this callback
		 * @param width
		 *            the width of the video
		 * @param height
		 *            the height of the video
		 */
		public void onVideoSizeChanged(MediaPlayer mp, int width, int height);
	}

	public interface OnErrorListener {
		/**
		 * Called to indicate an error.
		 * 
		 * @param mp
		 *            the MediaPlayer the error pertains to
		 * @param what
		 *            the type of error that has occurred:
		 *            <ul>
		 *            <li>{@link #MEDIA_ERROR_UNKNOWN}
		 *            <li>
		 *            {@link #MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK}
		 *            </ul>
		 * @param extra
		 *            an extra code, specific to the error. Typically
		 *            implementation dependant.
		 * @return True if the method handled the error, false if it didn't.
		 *         Returning false, or not having an OnErrorListener at all,
		 *         will cause the OnCompletionListener to be called.
		 */
		boolean onError(MediaPlayer mp, int what, int extra);
	}

	public interface OnInfoListener {
		/**
		 * Called to indicate an info or a warning.
		 * 
		 * @param mp
		 *            the MediaPlayer the info pertains to.
		 * @param what
		 *            the type of info or warning.
		 *            <ul>
		 *            <li>{@link #MEDIA_INFO_VIDEO_TRACK_LAGGING}
		 *            <li>{@link #MEDIA_INFO_BUFFERING_START}
		 *            <li>{@link #MEDIA_INFO_BUFFERING_END}
		 *            <li>{@link #MEDIA_INFO_NOT_SEEKABLE}
		 *            <li>{@link #MEDIA_INFO_DOWNLOAD_RATE_CHANGED}
		 *            </ul>
		 * @param extra
		 *            an extra code, specific to the info. Typically
		 *            implementation dependant.
		 * @return True if the method handled the info, false if it didn't.
		 *         Returning false, or not having an OnErrorListener at all,
		 *         will cause the info to be discarded.
		 */
		boolean onInfo(MediaPlayer mp, int what, int extra);
	}

	public interface OnTimedTextListener {
		/**
		 * Called to indicate that a text timed text need to display
		 * 
		 * @param text
		 *            the timedText to display
		 */
		public void onTimedText(String text);

		/**
		 * Called to indicate that an image timed text need to display
		 * 
		 * @param pixels
		 *            the pixels of the timed text image
		 * @param width
		 *            the width of the timed text image
		 * @param height
		 *            the height of the timed text image
		 */
		public void onTimedTextUpdate(byte[] pixels, int width, int height);
	}

	/**
	 * Class for MediaPlayer to return each audio/video/subtitle track's
	 * metadata.
	 * 
	 * @see io.vov.vitamio.MediaPlayer#getTrackInfo
	 */
	static public class TrackInfo {
		public static final int MEDIA_TRACK_TYPE_UNKNOWN = 0;
		public static final int MEDIA_TRACK_TYPE_VIDEO = 1;
		public static final int MEDIA_TRACK_TYPE_AUDIO = 2;
		public static final int MEDIA_TRACK_TYPE_TIMEDTEXT = 3;
		public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4;
		final int mTrackType;
		final SparseArray<MediaFormat> mTrackInfoArray;

		TrackInfo(int trackType, SparseArray<MediaFormat> trackInfoArray) {
			mTrackType = trackType;
			mTrackInfoArray = trackInfoArray;
		}

		/**
		 * Gets the track type.
		 * 
		 * @return TrackType which indicates if the track is video, audio, timed
		 *         text.
		 */
		public int getTrackType() {
			return mTrackType;
		}

		/**
		 * Gets the track info
		 * 
		 * @return map trackIndex to MediaFormat
		 */
		public SparseArray<MediaFormat> getTrackInfoArray() {
			return mTrackInfoArray;
		}
	}

	@SuppressLint("HandlerLeak")
	private class EventHandler extends Handler {
		private MediaPlayer mMediaPlayer;
		private Bundle mData;

		public EventHandler(MediaPlayer mp, Looper looper) {
			super(looper);
			mMediaPlayer = mp;
		}

		@Override
		public void handleMessage(Message msg) {
			switch (msg.what) {
			case MEDIA_PREPARED:
				if (mOnPreparedListener != null)
					mOnPreparedListener.onPrepared(mMediaPlayer);
				return;
			case MEDIA_PLAYBACK_COMPLETE:
				if (mOnCompletionListener != null)
					mOnCompletionListener.onCompletion(mMediaPlayer);
				stayAwake(false);
				return;
			case MEDIA_BUFFERING_UPDATE:
				if (mOnBufferingUpdateListener != null)
					mOnBufferingUpdateListener.onBufferingUpdate(mMediaPlayer,
							msg.arg1);
				return;
			case MEDIA_SEEK_COMPLETE:
				if (isPlaying())
					stayAwake(true);
				if (mOnSeekCompleteListener != null)
					mOnSeekCompleteListener.onSeekComplete(mMediaPlayer);
				return;
			case MEDIA_SET_VIDEO_SIZE:
				if (mOnVideoSizeChangedListener != null)
					mOnVideoSizeChangedListener.onVideoSizeChanged(
							mMediaPlayer, msg.arg1, msg.arg2);
				return;
			case MEDIA_ERROR:
				Log.e("Error (%d, %d)", msg.arg1, msg.arg2);
				boolean error_was_handled = false;
				if (mOnErrorListener != null)
					error_was_handled = mOnErrorListener.onError(mMediaPlayer,
							msg.arg1, msg.arg2);
				if (mOnCompletionListener != null && !error_was_handled)
					mOnCompletionListener.onCompletion(mMediaPlayer);
				stayAwake(false);
				return;
			case MEDIA_INFO:
//				Log.i("Info (%d, %d)", msg.arg1, msg.arg2);
				if (mOnInfoListener != null)
					mOnInfoListener.onInfo(mMediaPlayer, msg.arg1, msg.arg2);
				return;
			case MEDIA_CACHE:
				return;
			case MEDIA_TIMED_TEXT:
				mData = msg.getData();
				if (mData.getInt(MEDIA_SUBTITLE_TYPE) == SUBTITLE_TEXT) {
					Log.i("Subtitle : %s",
							mData.getString(MEDIA_SUBTITLE_STRING));
					if (mOnTimedTextListener != null)
						mOnTimedTextListener.onTimedText(mData
								.getString(MEDIA_SUBTITLE_STRING));
				} else if (mData.getInt(MEDIA_SUBTITLE_TYPE) == SUBTITLE_BITMAP) {
					Log.i("Subtitle : bitmap");
					if (mOnTimedTextListener != null)
						mOnTimedTextListener.onTimedTextUpdate(
								mData.getByteArray(MEDIA_SUBTITLE_BYTES),
								msg.arg1, msg.arg2);
				}
				return;
			case MEDIA_CACHING_UPDATE:
				if (mOnCachingUpdateListener != null) {
					int cacheType = msg.getData().getInt(MEDIA_CACHING_TYPE);
					if (cacheType == CACHE_TYPE_NOT_AVAILABLE) {
						mOnCachingUpdateListener.onCachingNotAvailable(
								mMediaPlayer,
								msg.getData().getInt(MEDIA_CACHING_INFO));
					} else if (cacheType == CACHE_TYPE_UPDATE) {
						mOnCachingUpdateListener.onCachingUpdate(
								mMediaPlayer,
								msg.getData().getLongArray(
										MEDIA_CACHING_SEGMENTS));
					} else if (cacheType == CACHE_TYPE_SPEED) {
						mOnCachingUpdateListener.onCachingSpeed(mMediaPlayer,
								msg.getData().getInt(MEDIA_CACHING_INFO));
					} else if (cacheType == CACHE_TYPE_START) {
						mOnCachingUpdateListener.onCachingStart(mMediaPlayer);
					} else if (cacheType == CACHE_TYPE_COMPLETE) {
						mOnCachingUpdateListener
								.onCachingComplete(mMediaPlayer);
					}
				}
				return;
			case MEDIA_NOP:
				return;
			case MEDIA_HW_ERROR:
				if (mOnHWRenderFailedListener != null)
					mOnHWRenderFailedListener.onFailed();
				return;
			default:
				Log.e("Unknown message type " + msg.what);
				return;
			}
		}
	}
}
